/**
 * 
 */
package org.wicket.jbpm.engine.core;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jbpm.JbpmContext;
import org.jbpm.JbpmException;
import org.jbpm.db.GraphSession;
import org.jbpm.db.TaskMgmtSession;
import org.jbpm.graph.def.Node;
import org.jbpm.graph.def.ProcessDefinition;
import org.jbpm.graph.def.Transition;
import org.jbpm.graph.exe.ExecutionContext;
import org.jbpm.graph.exe.ProcessInstance;
import org.jbpm.graph.exe.Token;
import org.jbpm.job.Timer;
import org.jbpm.taskmgmt.def.Task;
import org.jbpm.taskmgmt.exe.TaskInstance;
import org.springmodules.workflow.jbpm31.JbpmCallback;
import org.springmodules.workflow.jbpm31.JbpmTemplate;
import org.wicket.jbpm.engine.exception.WicketJbpmException;
import org.wicket.jbpm.engine.spring.JbpmCoreEngine;
import org.wicket.jbpm.engine.utils.JbpmEngineUtils;
import org.wicket.jbpm.engine.workflow.WfPath;
import org.wicket.jbpm.engine.workflow.WfTask;

/**
 * @author Emmanuel Nollase - emanux created: Jun 27, 2009 - 2:03:36 PM
 */
public class JbpmCoreEngineImpl extends BPMEngine implements JbpmCoreEngine
{

    private final static Log log = LogFactory.getLog(JbpmCoreEngineImpl.class);

    private final static String WORKFLOW_PATH_SEPERATOR = "-";

    private final static String WORKFLOW_TOKEN_SEPERATOR = "@";

    protected JbpmTemplate jbpmTemplate;

    public void setJbpmTemplate(JbpmTemplate jbpmTemplate)
    {
	this.jbpmTemplate = jbpmTemplate;
    }

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#endTask(java.lang.String, java.lang.String, java.util.Map)
     */
    public WfTask endTask(String taskInstanceId, String comments,
	    Map<String, Serializable> vars) throws WicketJbpmException
    {
	return endTask(taskInstanceId, null, comments, vars);
    }

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#endTask(java.lang.String, java.lang.String)
     */
    public WfTask endTask(String taskInstanceId, String comments)
	    throws WicketJbpmException
    {
	return endTask(taskInstanceId, null, comments, null);
    }

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#signalTask(java.lang.String)
     */
    public WfPath signalTask(final String pathId) throws WicketJbpmException
    {
	return signal(pathId, null,null);
    }
    
    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#signalTask(java.lang.String, java.lang.String)
     */
    public WfPath signalTask(final String pathId, final String transition)
    	throws WicketJbpmException
    {
	return signal(pathId, transition,null);
    }
    
    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#signalTask(java.lang.String, java.util.Map)
     */
    public WfPath signalTask(final String pathId,final Map<String, Serializable> vars) throws WicketJbpmException
    {
	return signal(pathId, null,vars);
    }
    
    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#signalTask(java.lang.String, java.lang.String, java.util.Map)
     */
    public WfPath signalTask(final String pathId,final String transition,final Map<String, Serializable> vars) throws WicketJbpmException
    {
	return signal(pathId, transition,vars);
    }

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#endJbpmWorkflow(java.lang.String)
     */
    public ProcessInstance endJbpmWorkflow(final String procId)
	    throws WicketJbpmException
    {
	try
	{
	    return (ProcessInstance) jbpmTemplate.execute(new JbpmCallback()
	    {

		public Object doInJbpm(JbpmContext context)
		{
		    // retrieve and cancel process instance
		    GraphSession graphSession = context.getGraphSession();
		    ProcessInstance processInstance = graphSession
			    .getProcessInstance(JbpmEngineUtils
				    .getJbpmId(procId));
		    processInstance.getContextInstance().setVariable(
			    "cancelled", true);
		    processInstance.end();

		    // delete the process instance
		    graphSession.deleteProcessInstance(processInstance, true,
			    true);
		    return processInstance;
		}
	    });
	} catch (JbpmException e)
	{
	    throw new WicketJbpmException(
		    "Failed to cancel workflow instance '" + procId + "'", e);
	}

    }

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#getTaskLeavingTransition(java.lang.String)
     */
    @SuppressWarnings("unchecked")
    public List<String> getTaskLeavingTransition(final String taskInstanceId)
    {
	try
	{
	    return (List<String>) jbpmTemplate.execute(new JbpmCallback()
	    {

		public Object doInJbpm(JbpmContext context)
		{
		    final List<String> transitions = new ArrayList<String>();
		    final TaskMgmtSession taskSession = context
			    .getTaskMgmtSession();
		    final TaskInstance taskInstance = taskSession
			    .getTaskInstance(JbpmEngineUtils
				    .getJbpmId(taskInstanceId));

		    final List<Transition> trans = taskInstance.getToken()
			    .getNode().getLeavingTransitions();

		    for (Transition transition : trans)
		    {
			transitions.add(transition.getName());
		    }
		    return transitions;
		}
	    });
	} catch (JbpmException e)
	{
	    throw new WicketJbpmException(
		    "Failed to get transition  from workflow task '"
			    + taskInstanceId + "' : " + e.getMessage(), e);

	}
    }
    
    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#getAssignedTasks(java.lang.String)
     */
    @SuppressWarnings("unchecked")
    public List<WfTask> getAssignedTasks(final String actorId)
    {
	try
	{
	    return (List<WfTask>) jbpmTemplate.execute(new JbpmCallback()
	    {
		public Object doInJbpm(JbpmContext context)
		{
		    List<TaskInstance> tasks = null;
		    final TaskMgmtSession taskSession = context.getTaskMgmtSession();
		    tasks = taskSession.findPooledTaskInstances(actorId);

		    final List<WfTask> workflowTasks = new ArrayList<WfTask>(tasks.size());
		    for (TaskInstance task : tasks)
		    {
			WfTask wfTask = JbpmEngineUtils.createWorkflowTask(engineId, task);
			workflowTasks.add(wfTask);
		    }
		    return workflowTasks;
		}
	    });
	} catch (JbpmException e)
	{
	    throw new WicketJbpmException("Failed to get assigned task for '"
		    + actorId + "'", e);
	}
    }

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#startProcessInstance(org.jbpm.graph.def.ProcessDefinition, java.lang.String, java.util.Map)
     */
    public WfPath startProcessInstance(final ProcessDefinition processDefinition, final String userId,
	    final Map<String, Serializable> instanceParameters)
    {
	try
	{
	    return (WfPath) jbpmTemplate.execute(new JbpmCallback()
	    {
		public Object doInJbpm(JbpmContext context)
		{

		    context.setActorId(userId);
		    final ProcessInstance processInstance = new ProcessInstance(processDefinition);
		    processInstance.setKey(JbpmEngineUtils.genearteUID());

		    // create the start task if one exists
		    Token token = processInstance.getRootToken();
		    final Task startTask = processInstance.getTaskMgmtInstance()
			    .getTaskMgmtDefinition().getStartTask();
		    if (startTask != null)
		    {
			final TaskInstance taskInstance = processInstance
				.getTaskMgmtInstance()
				.createStartTaskInstance();
			taskInstance.setVariables(instanceParameters);
			token = taskInstance.getToken();
		    }

		    // processInstance.signal();
		    // Save the process instance along with the task instance
		    context.save(processInstance);

		    return JbpmEngineUtils.createWorkflowPath(token);
		}
	    });
	} catch (JbpmException e)
	{
	    throw new WicketJbpmException(
		    "Failed to retrieve workflow instance", e);
	}
    }

    
    /*************** private methods & other core methods *************************************/

    protected WfTask endTask(final String taskInstanceId,
	    final String transitionName, final String comments,
	    final Map<String, Serializable> vars)
    {

	try
	{
	    return (WfTask) jbpmTemplate.execute(new JbpmCallback()
	    {

		public Object doInJbpm(JbpmContext context)
		{
		    TaskMgmtSession taskSession = context.getTaskMgmtSession();
		    TaskInstance taskInstance = taskSession
			    .getTaskInstance(JbpmEngineUtils
				    .getJbpmId(taskInstanceId));
		    // check if the comment is empty
		    if (StringUtils.isNotEmpty(comments))
		    {
			taskInstance.addComment(comments);
		    }
		    // check if the variables are empty
		    if (null != vars)
		    {
			taskInstance.setVariables(vars);
		    }
		    // signal the transition on the task
		    if (transitionName == null)
		    {
			taskInstance.end();
		    } 
		    else
		    {
			Node node = taskInstance.getToken().getNode();
			if (node.getLeavingTransition(transitionName) == null)
			{
			    throw new JbpmException("Transition '"
				    + transitionName
				    + "' is invalid for Workflow task '"
				    + taskInstanceId + "'");
			}
			taskInstance.end(transitionName);
		    }

		    ProcessInstance processInstance = taskInstance.getToken()
			    .getProcessInstance();
		    context.save(processInstance);

		    return JbpmEngineUtils.createWorkflowTask(engineId,
			    taskInstance);
		}
	    });
	} catch (JbpmException e)
	{
	    throw new WicketJbpmException("Failed to signal transition '"
		    + transitionName + "' from workflow task '"
		    + taskInstanceId + "' : " + e.getMessage(), e);

	}
    }

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#endTimerTask(org.jbpm.graph.exe.ExecutionContext)
     */
    public void endTimerTask(ExecutionContext context)
	    throws WicketJbpmException
    {

	try
	{
	   Timer timer = context.getTimer();
	   String trans = timer.getTransitionName();
	   log.info("Timeout transition: "+trans);
	   context.getToken().signal(trans);
	    
	   final Collection<TaskInstance> tasks = context
		    .getTaskMgmtInstance().getTaskInstances();
	    for (TaskInstance tasksInts : tasks)
	    {

		if (!tasksInts.hasEnded())
		{
		    tasksInts.end();
		    log.info("task name: " + tasksInts.getName() + " has ended.");
		}
	    }
	    // finally save the process instance contain here in timers
	    context.getJbpmContext().save(
		    context.getToken().getProcessInstance());
	} catch (JbpmException e)
	{
	    throw new WicketJbpmException("Failed to execute scheduled task.",
		    e);
	}
    }

    public WfPath signal(final String pathId, final String transition,final Map<String, Serializable> vars)
    {

	String tmpTrans = null;
	if (transition != null && StringUtils.isNotBlank(transition))
	{
	    tmpTrans = new String(transition);
	}
	final String transitions = tmpTrans;
	try
	{
	    return (WfPath) jbpmTemplate.execute(new JbpmCallback()
	    {

		public Object doInJbpm(JbpmContext context)
		{
		    GraphSession graphSession = context.getGraphSession();
		    Token token = getWorkflowToken(graphSession, pathId);
		    
		    // get the process instance associated with token
		    ProcessInstance processInstance = token.getProcessInstance();
		    
		    // if there is variables to be set. SET IT FIRST BEFORE SIGNALLING TO NEXT TRANSITION
		    if( vars != null)
		    {
		    	processInstance.getContextInstance().addVariables(vars, token);
		    }
		    
		    // signal the transition
		    if (transitions == null)
		    {
			token.signal();
		    } else
		    {
			Node node = token.getNode();
			if (!node.hasLeavingTransition(transitions))
			{
			    throw new JbpmException("Transition '"
				    + transitions
				    + "' is invalid for Workflow Path Id '"
				    + pathId + "'");
			}
			token.signal(transitions);
		    }
		    
		    context.save(processInstance);
		    // return new workflow path
		    return JbpmEngineUtils.createWorkflowPath(token);

		}
	    });
	} catch (JbpmException e)
	{
	    throw new WicketJbpmException("Failed to signal transition '"
		    + transition + "' from workflow path id '" + pathId
		    + "' : " + e.getMessage(), e);

	}
    }

    /*
     * (non-Javadoc)
     * @see org.wicket.jbpm.engine.spring.JbpmCoreEngine#reassignTask(java.lang.String, java.lang.String[], java.lang.String)
     */
    public WfTask reassignTask(final String taskInstanceId,
	    final String[] pooledActors, final String comments)
	    throws JbpmException, WicketJbpmException
    {
	try
	{
	    return (WfTask) jbpmTemplate.execute(new JbpmCallback()
	    {

		public Object doInJbpm(JbpmContext context)
			throws JbpmException
		{
		    TaskMgmtSession taskSession = context.getTaskMgmtSession();
		    TaskInstance taskInstance = taskSession
			    .getTaskInstance(JbpmEngineUtils
				    .getJbpmId(taskInstanceId));

		    if (StringUtils.isNotEmpty(comments))
		    {
			taskInstance.addComment(comments);
		    }

		    if (pooledActors != null && pooledActors.length > 0)
		    {
			taskInstance.setPooledActors(pooledActors);
		    }

		    context.save(taskInstance.getProcessInstance());
		    final TaskInstance instance = taskSession
			    .getTaskInstance(JbpmEngineUtils
				    .getJbpmId(taskInstanceId));
		    return JbpmEngineUtils.createWorkflowTask(engineId,
			    instance);
		}
	    });
	} catch (Exception ex)
	{
	    throw new WicketJbpmException("Failed to re-assign task id: "
		    + taskInstanceId + " to: " + pooledActors, ex);
	}
    }

    /**
     * Get the JBoss JBPM Token for the Workflow Path
     * 
     * @param session
     *            JBoss JBPM Graph Session
     * @param pathId
     *            workflow path id
     * @return JBoss JBPM Token
     */
    protected Token getWorkflowToken(GraphSession session, String pathId)
    {
	// extract process id and token path within process
	String[] path = pathId.split(WORKFLOW_PATH_SEPERATOR);
	if (path.length != 2)
	{
	    throw new RuntimeException("Invalid workflow path '" + pathId + "'");
	}

	// retrieve jBPM token for workflow position
	ProcessInstance processInstance = getProcessInstance(session, path[0]);
	String tokenId = path[1].replace(WORKFLOW_TOKEN_SEPERATOR, "/");
	Token token = processInstance.findToken(tokenId);
	if (token == null)
	{
	    throw new RuntimeException("Workflow path '" + pathId
		    + "' does not exist");
	}

	return token;
    }

    protected ProcessInstance getProcessInstance(GraphSession graphSession,
	    String workflowId)
    {
	ProcessInstance processInstance = graphSession
		.getProcessInstance(JbpmEngineUtils.getJbpmId(workflowId));
	if (processInstance == null)
	{
	    throw new RuntimeException("Workflow instance '" + workflowId
		    + "' does not exist");
	}
	return processInstance;
    }
}
